Imports System
Imports System.Text
Imports System.Data
Imports System.Collections.Generic

Imports Framework

#Region "Enum: Primary Key Values"
'<CLSCompliant(True)> _
'Public Enum EProduct
'    Huey = 1
'    Duey = 2
'    Louie = 3
'End Enum
#End Region

'Table-Row Class (Customisable half)
Partial Public Class CProduct

#Region "Constants"
    Public Const VIEW_RELATED As String = "vwMcm_RelatedProducts"
    Public Shared JOIN_RELATED As String = String.Concat(TABLE_NAME, " INNER JOIN ", VIEW_RELATED, " ON ProductId=RelatedProductId")
#End Region

#Region "Constructors (Public)"
    'Default Connection String
    Public Sub New()
        MyBase.New()
    End Sub
    
    'Alternative Connection String
    Public Sub New(ByVal dataSrc as CDataSrc)
        MyBase.New(dataSrc)
    End Sub
    
    'Hidden (UI code should use cache instead)
    Protected Friend Sub New([productId] As Integer)
        MyBase.New([productId])
    End Sub
    Protected Friend Sub New(ByVal dataSrc As CDataSrc, [productId] As Integer)
        MyBase.New(dataSrc, [productId])
    End Sub
    Protected Friend Sub New(ByVal dataSrc As CDataSrc, ByVal [productId] As Integer, ByVal txOrNull As IDbTransaction)
        MyBase.New(dataSrc, [productId], txOrNull)
    End Sub
#End Region

#Region "Default Values"
    Protected Overrides Sub InitValues_Custom()
        m_productCreated = DateTime.Now
        m_productImage = Nothing
        m_productThumb = Nothing
        m_productBelongsToSortOrder = 0
        m_productLanguageId = ELanguage.English
        m_productSpeakerId = ESpeaker.MikeConnell
        m_productLocationId = ELocation.Baycity
    End Sub 
#End Region

#Region "Default Connection String"
    Protected Overrides Function DefaultDataSrc() As CDataSrc
        Return CDataSrc.Default
    End Function
#End Region

#Region "Properties - Relationships"    
    'Relationships - Foriegn Keys (e.g parent)
    Public ReadOnly Property [ProductType]() As CProductType
        Get
            Return CProductType.Cache.GetById(Me.ProductTypeId)
        End Get
    End Property
    Public ReadOnly Property BelongsTo() As CProduct
        Get
            Return CProduct.Cache.GetById(Me.ProductBelongsToProductId)
        End Get
    End Property

    Public ReadOnly Property [Language]() As CLanguage
        Get
            Return CLanguage.Cache.GetById(Me.ProductLanguageId)
        End Get
    End Property
    Public ReadOnly Property [Speaker]() As CSpeaker
        Get
            Return CSpeaker.Cache.GetById(Me.ProductSpeakerId)
        End Get
    End Property
    Public ReadOnly Property [Speaker2]() As CSpeaker
        Get
            Return CSpeaker.Cache.GetById(Me.ProductSpeaker2Id)
        End Get
    End Property
    Public ReadOnly Property [Location]() As CLocation
        Get
            Return CLocation.Cache.GetById(Me.ProductLocationId)
        End Get
    End Property
    Public ReadOnly Property [Scribe]() As CScribe
        Get
            Return CScribe.Cache.GetById(Me.ProductScribeId)
        End Get
    End Property
    Public ReadOnly Property [ImageScribe]() As CScribe
        Get
            Return CScribe.Cache.GetById(Me.ProductImageScribeId)
        End Get
    End Property

    Public Property [Button]() As CButton
        Get
            Return CButton.Cache.GetById(Me.ProductPaypalButtonId)
        End Get
        Set(value As CButton)
            If IsNothing(value) Then
                Me.ProductPaypalButtonId = Integer.MinValue
            Else
                Me.ProductPaypalButtonId = value.ButtonId
            End If
        End Set
    End Property

    'Relationships - Collections (e.g. children)
    Public ReadOnly Property Children As CProductList
        Get
            Return CProduct.Cache.GetByBelongsToProductId(Me.ProductId)
        End Get
    End Property
    Protected Function Children_(ByVal tx As IDbTransaction) As CProductList   'Only used for cascade deletes
        With New CProduct(DataSrc)
            Return .SelectByBelongsToProductId(Me.ProductId, tx)
        End With
    End Function



    Public ReadOnly Property [Files] As CFileList
        Get
            If Not IsSet Then Return CFile.Cache.GetByProductId(Me.ProductId)
            Dim list As New CFileList(CFile.Cache.GetByProductId(Me.ProductId))
            For Each i As CProduct In Me.Children
                list.AddRange(i.Files)
            Next
            Return list
        End Get
    End Property
    Protected Function Files_(ByVal tx As IDbTransaction) As CFileList   'Only used for cascade deletes
        With New CFile(DataSrc)
            Return .SelectByProductId(Me.ProductId, tx)
        End With
    End Function



    Public Function [Orders]() As COrderList
        Return New COrder(DataSrc).SelectByProductId(Me.ProductId)
    End Function
    Public Function [Orders](pi As CPagingInfo) As COrderList
        Return New COrder(DataSrc).SelectByProductId(pi, Me.ProductId)
    End Function
    Friend Function [Orders_](ByVal tx As IDbTransaction) As COrderList
        Return New COrder(DataSrc).SelectByProductId(Me.ProductId, tx)     'Only used for cascade deletes
    End Function
    Public Function OrdersCount() As Integer
        Return New COrder(DataSrc).SelectCountByProductId(Me.ProductId)
    End Function

    Public Function PaymentsCount() As Integer
        Return New CPayment(DataSrc).SelectCountByProduct(Me.ProductId)
    End Function
    Public Function PaymentsSumOk() As Decimal
        Return New CPayment(DataSrc).SelectSumOkByProduct(Me.ProductId)
    End Function
    Public Function SalesValue() As Decimal
        Return New COrder(DataSrc).SalesValueForProduct(Me.ProductId)
    End Function
    Public Function SalesPaid() As Decimal
        Return New COrder(DataSrc).SalesPaidForProduct(Me.ProductId)
    End Function

    Public Function [Suggestions]() As CSuggestionList
        Return New CSuggestion(DataSrc).SelectByProductId(Me.ProductId)
    End Function
    Public Function [Suggestions](pi As CPagingInfo) As CSuggestionList
        Return New CSuggestion(DataSrc).SelectByProductId(pi, Me.ProductId)
    End Function
    Friend Function [Suggestions](ByVal tx As IDbTransaction) As CSuggestionList
        Return New CSuggestion(DataSrc).SelectByProductId(Me.ProductId, tx)     'Only used for cascade deletes
    End Function
    Public Function SuggestionsCount() As Integer
        Return New CSuggestion(DataSrc).SelectCountByProductId(Me.ProductId)
    End Function

    Public Function SuggestionsCountPending() As Integer
        Return New CSuggestion(DataSrc).CountPending(Me.ProductId)
    End Function

    Public Function [Historys]() As CHistoryList
        Return New CHistory(DataSrc).SelectByProductId(Me.ProductId)
    End Function
    Public Function [Historys](pi As CPagingInfo) As CHistoryList
        Return New CHistory(DataSrc).SelectByProductId(pi, Me.ProductId)
    End Function
    Friend Function [Historys](ByVal tx As IDbTransaction) As CHistoryList
        Return New CHistory(DataSrc).SelectByProductId(Me.ProductId, tx)     'Only used for cascade deletes
    End Function
    Public Function HistorysCount() As Integer
        Return New CHistory(DataSrc).SelectCountByProductId(Me.ProductId)
    End Function


#End Region

#Region "Properties - Customisation"
    Private m_chapters As List(Of String)
    Private m_chaptersChineseSimp As List(Of String)
    Private m_chaptersChineseTrad As List(Of String)

    Private Shared CHAPTER_MIN_SIZE = 350 'Concerns last page only
    Private Shared CHAPTER_TARGET_SIZE = 3500 'Min size of a typical page


    Private Shared CHAPTER_MIN_SIZE_CHINESE = 150 'Concerns last page only
    Private Shared CHAPTER_TARGET_SIZE_CHINESE = 1500 'Min size of a typical page

    Public ReadOnly Property Chapters As List(Of String)
        Get
            If IsNothing(m_chapters) Then
                If Not HasTranscript Then
                    m_chapters = New List(Of String)(0)
                    Return m_chapters
                End If

                SyncLock Me
                    If IsNothing(m_chapters) Then
                        Dim list As New List(Of String)
                        Dim sb As New StringBuilder
                        For Each i As String In Paragraphs
                            sb.AppendLine(i).AppendLine()
                            If sb.Length > CHAPTER_TARGET_SIZE Then
                                list.Add(sb.ToString)
                                sb.Clear()
                            End If
                        Next
                        If sb.Length < CHAPTER_MIN_SIZE Then
                            sb.Insert(0, vbCrLf)
                            sb.Insert(0, vbCrLf)
                            If list.Count > 0 Then list(list.Count - 1) &= sb.ToString Else list.Add(sb.ToString)
                        Else
                            list.Add(sb.ToString)
                        End If


                        'Make bigger pages, if there's too many
                        If list.Count > 10 Then
                            Dim offset As Integer = 500
                            If list.Count > 15 Then offset = 1000
                            If list.Count > 20 Then offset = 1500
                            If list.Count > 25 Then offset = 2000
                            If list.Count > 30 Then offset = 2500
                            list.Clear()
                            sb.Clear()
                            For Each i As String In Paragraphs
                                sb.AppendLine(i).AppendLine()
                                If sb.Length > CHAPTER_TARGET_SIZE + offset Then
                                    list.Add(sb.ToString)
                                    sb.Clear()
                                End If
                            Next
                            If sb.Length < CHAPTER_MIN_SIZE Then
                                list(list.Count - 1) &= sb.ToString
                            Else
                                list.Add(sb.ToString)
                            End If
                        End If


                        m_chapters = list
                    End If
                End SyncLock
            End If
            Return m_chapters
        End Get
    End Property

    Public ReadOnly Property ChaptersChineseSimp As List(Of String)
        Get
            If IsNothing(m_chaptersChineseSimp) Then
                If Not HasTranscriptChineseSimp Then
                    m_chaptersChineseSimp = New List(Of String)(0)
                    Return m_chaptersChineseSimp
                End If

                SyncLock Me
                    If IsNothing(m_chaptersChineseSimp) Then
                        Dim list As New List(Of String)
                        Dim sb As New StringBuilder
                        For Each i As String In ParagraphsChineseSimp
                            sb.AppendLine(i).AppendLine()
                            If sb.Length > CHAPTER_TARGET_SIZE_CHINESE Then
                                list.Add(sb.ToString)
                                sb.Clear()
                            End If
                        Next
                        If sb.Length < CHAPTER_MIN_SIZE_CHINESE Then
                            sb.Insert(0, vbCrLf)
                            sb.Insert(0, vbCrLf)
                            If list.Count > 0 Then list(list.Count - 1) &= sb.ToString Else list.Add(sb.ToString)
                        Else
                            list.Add(sb.ToString)
                        End If


                        'Make bigger pages, if there's too many
                        If list.Count > 10 Then
                            Dim offset As Integer = 500
                            If list.Count > 15 Then offset = 1000
                            If list.Count > 20 Then offset = 1500
                            If list.Count > 25 Then offset = 2000
                            If list.Count > 30 Then offset = 2500
                            list.Clear()
                            sb.Clear()
                            For Each i As String In ParagraphsChineseSimp
                                sb.AppendLine(i).AppendLine()
                                If sb.Length > CHAPTER_TARGET_SIZE_CHINESE + offset Then
                                    list.Add(sb.ToString)
                                    sb.Clear()
                                End If
                            Next
                            If sb.Length < CHAPTER_MIN_SIZE_CHINESE Then
                                list(list.Count - 1) &= sb.ToString
                            Else
                                list.Add(sb.ToString)
                            End If
                        End If


                        m_chaptersChineseSimp = list
                    End If
                End SyncLock
            End If
            Return m_chaptersChineseSimp
        End Get
    End Property

    Public ReadOnly Property ChaptersChineseTrad As List(Of String)
        Get
            If IsNothing(m_chaptersChineseTrad) Then
                If Not HasTranscriptChineseTrad Then
                    m_chaptersChineseTrad = New List(Of String)(0)
                    Return m_chaptersChineseTrad
                End If

                SyncLock Me
                    If IsNothing(m_chaptersChineseTrad) Then
                        Dim list As New List(Of String)
                        Dim sb As New StringBuilder
                        For Each i As String In ParagraphsChineseTrad
                            sb.AppendLine(i).AppendLine()
                            If sb.Length > CHAPTER_TARGET_SIZE_CHINESE Then
                                list.Add(sb.ToString)
                                sb.Clear()
                            End If
                        Next
                        If sb.Length < CHAPTER_MIN_SIZE_CHINESE Then
                            sb.Insert(0, vbCrLf)
                            sb.Insert(0, vbCrLf)
                            If list.Count > 0 Then list(list.Count - 1) &= sb.ToString Else list.Add(sb.ToString)
                        Else
                            list.Add(sb.ToString)
                        End If


                        'Make bigger pages, if there's too many
                        If list.Count > 10 Then
                            Dim offset As Integer = 500
                            If list.Count > 15 Then offset = 1000
                            If list.Count > 20 Then offset = 1500
                            If list.Count > 25 Then offset = 2000
                            If list.Count > 30 Then offset = 2500
                            list.Clear()
                            sb.Clear()
                            For Each i As String In ParagraphsChineseTrad
                                sb.AppendLine(i).AppendLine()
                                If sb.Length > CHAPTER_TARGET_SIZE_CHINESE + offset Then
                                    list.Add(sb.ToString)
                                    sb.Clear()
                                End If
                            Next
                            If sb.Length < CHAPTER_MIN_SIZE_CHINESE Then
                                list(list.Count - 1) &= sb.ToString
                            Else
                                list.Add(sb.ToString)
                            End If
                        End If


                        m_chaptersChineseTrad = list
                    End If
                End SyncLock
            End If
            Return m_chaptersChineseTrad
        End Get
    End Property


    Public ReadOnly Property Paragraphs As List(Of String)
        Get
            Return CUtilities.SplitOn(ProductTranscription, vbCrLf & vbCrLf)
        End Get
    End Property
    Public ReadOnly Property ParagraphsChineseSimp As List(Of String)
        Get
            Return CUtilities.SplitOn(ProductTranscriptionChineseSimplified, vbCrLf & vbCrLf)
        End Get
    End Property
    Public ReadOnly Property ParagraphsChineseTrad As List(Of String)
        Get
            Return CUtilities.SplitOn(ProductTranscriptionChineseTraditional, vbCrLf & vbCrLf)
        End Get
    End Property

    Public ReadOnly Property ProductNameChineseTraditionalOrSimplified
        Get
            Dim s As String = ProductNameChineseTraditional
            If String.IsNullOrEmpty(s) Then s = ProductNameChineseSimplified
            Return s
        End Get
    End Property
    Public ReadOnly Property ProductDescriptionChineseTraditionalOrSimplified
        Get
            Dim s As String = ProductDescriptionChineseTraditional
            If String.IsNullOrEmpty(s) Then s = ProductDescriptionChineseSimplified
            Return s
        End Get
    End Property
    Public ReadOnly Property ProductKeywordsChineseTraditionalOrSimplified
        Get
            Dim s As String = ProductKeywordsChineseTraditional
            If String.IsNullOrEmpty(s) Then s = ProductKeywordsChineseSimplified
            Return s
        End Get
    End Property

    Private m_transcriptionLower As String
    Public ReadOnly Property ProductTranscriptionLower() As String
        Get
            If String.IsNullOrEmpty(m_productTranscription) Then Return String.Empty
            If IsNothing(m_transcriptionLower) OrElse m_transcriptionLower.Length <> m_productTranscription.Length Then
                m_transcriptionLower = m_productTranscription.ToLower
            End If
            Return m_transcriptionLower
        End Get
    End Property

    Public ReadOnly Property Description_ As String
        Get
            If Not String.IsNullOrEmpty(ProductDescription) Then Return ProductDescription
            If HasParent Then Return BelongsTo.ProductDescription
            If Children.Count = 0 Then Return String.Empty

            'From individual sets (add in chinese here, instead of showing it separately below)
            Dim sb As New StringBuilder
            For Each i As CProduct In Children
                sb.Append(i.ProductName).Append(" ").Append(i.ProductDescription).AppendLine()
                If Not String.IsNullOrEmpty(i.ProductDescription) Then sb.AppendLine()

                If Not i.IsEnglish Then
                    If Not String.IsNullOrEmpty(i.ProductNameChineseSimplified) Then
                        sb.Append(i.ProductNameChineseSimplified).Append(" ").Append(i.ProductDescriptionChineseSimplified).AppendLine()
                        If Not String.IsNullOrEmpty(i.ProductDescriptionChineseSimplified) Then sb.AppendLine()
                    End If
                End If
            Next
            Return sb.ToString
        End Get
    End Property
    Public ReadOnly Property DescriptionChineseSimp_ As String
        Get
            If Not String.IsNullOrEmpty(ProductDescriptionChineseSimplified) Then Return ProductDescriptionChineseSimplified
            If HasParent Then Return BelongsTo.ProductDescriptionChineseSimplified
            Return String.Empty
        End Get
    End Property
    Public ReadOnly Property DescriptionChineseTrad_ As String
        Get
            If Not String.IsNullOrEmpty(ProductDescriptionChineseTraditionalOrSimplified) Then Return ProductDescriptionChineseTraditionalOrSimplified
            If HasParent Then Return BelongsTo.ProductDescriptionChineseTraditionalOrSimplified
            Return String.Empty
        End Get
    End Property
    Public ReadOnly Property Description_Br As String
        Get
            Return System.Web.HttpUtility.HtmlEncode(Description_).Replace(vbCrLf, "<br/>")
        End Get
    End Property
    Public ReadOnly Property DescriptionChineseSimp_Br As String
        Get
            Return System.Web.HttpUtility.HtmlEncode(DescriptionChineseSimp_).Replace(vbCrLf, "<br/>")
        End Get
    End Property
    Public ReadOnly Property DescriptionChineseTrad_Br As String
        Get
            Return System.Web.HttpUtility.HtmlEncode(DescriptionChineseTrad_).Replace(vbCrLf, "<br/>")
        End Get
    End Property

    Public ReadOnly Property DateAndName As String
        Get
            Return String.Concat(ProductName, " (", DateString, ")")
        End Get
    End Property
    Public ReadOnly Property DateAndNameShort As String
        Get
            Return CUtilities.Truncate(DateAndName, 100)
        End Get
    End Property

    'Cached totals
    Public ReadOnly Property OrdersCountCached As Integer 'Number of orders
        Get
            Dim key As String = String.Concat("OrdersCountCached", ProductId)
            Dim obj As Object = CCache.Get(key)
            If Not IsNothing(obj) Then Return obj
            OrdersCountCached = OrdersCount()
            CCache.Set(key, OrdersCountCached, TimeSpan.FromMinutes(3))
        End Get
    End Property
    Public ReadOnly Property SalesValueCached As Decimal 'Value if all were paid
        Get
            Dim key As String = String.Concat("SalesValueCached", ProductId)
            Dim obj As Object = CCache.Get(key)
            If Not IsNothing(obj) Then Return obj
            SalesValueCached = SalesValue()
            CCache.Set(key, SalesValueCached, TimeSpan.FromMinutes(3))
        End Get
    End Property
    Public ReadOnly Property SalesPaidCached As Decimal 'Value of orders paid in full (excludes donations)
        Get
            Dim key As String = String.Concat("SalesPaidCached", ProductId)
            Dim obj As Object = CCache.Get(key)
            If Not IsNothing(obj) Then Return obj
            SalesPaidCached = SalesPaid()
            CCache.Set(key, SalesPaidCached, TimeSpan.FromMinutes(3))
        End Get
    End Property
    Public ReadOnly Property PaymentsCountCached As Integer 'Number of payments or donations
        Get
            Dim key As String = String.Concat("PaymentsCountCached", ProductId)
            Dim obj As Object = CCache.Get(key)
            If Not IsNothing(obj) Then Return obj
            PaymentsCountCached = PaymentsCount()
            CCache.Set(key, PaymentsCountCached, TimeSpan.FromMinutes(3))
        End Get
    End Property
    Public ReadOnly Property PaymentsSumOkCached As Decimal 'value of payments + donations
        Get
            Dim key As String = String.Concat("PaymentsSumOkCached", ProductId)
            Dim obj As Object = CCache.Get(key)
            If Not IsNothing(obj) Then Return obj
            PaymentsSumOkCached = PaymentsSumOk()
            CCache.Set(key, PaymentsSumOkCached, TimeSpan.FromMinutes(3))
        End Get
    End Property


    'Shortcuts derived from parent properties
    Public ReadOnly Property IsFree As Boolean
        Get
            Return ProductType.TypeIsFree
        End Get
    End Property
    Public ReadOnly Property IsDonateButton As Boolean
        Get
            Return ProductPaypalButtonId = EButton.Donate
        End Get
    End Property
    Public ReadOnly Property CanHaveAudio As Boolean
        Get
            Return ProductType.TypeCanHaveAudio
        End Get
    End Property
    Public ReadOnly Property CanHaveHtmlNotesOrPreview As Boolean
        Get
            Return ProductType.TypeCanHaveHtmlNotesOrPreview
        End Get
    End Property
    Public ReadOnly Property CanHavePdfOrWord As Boolean
        Get
            Return ProductType.TypeCanHavePdfOrWord
        End Get
    End Property
    Public ReadOnly Property CanHaveTranscript As Boolean
        Get
            Return ProductType.TypeCanHaveTranscript
        End Get
    End Property
    Public ReadOnly Property CanHaveVimeo As Boolean
        Get
            Return ProductType.TypeCanHaveVimeo
        End Get
    End Property
    Public ReadOnly Property CanHaveParent As Boolean
        Get
            Return ParentTypeId > 0
        End Get
    End Property
    Public ReadOnly Property ParentIsRequired As Boolean
        Get
            Return ProductType.TypeParentIsRequired
        End Get
    End Property
    Public ReadOnly Property ParentTypeId As Byte
        Get
            Return ProductType.TypeParentTypeId
        End Get
    End Property
    Public ReadOnly Property TypeName As String
        Get
            Return ProductType.TypeName
        End Get
    End Property
    Public ReadOnly Property Plural As String
        Get
            Return ProductType.Plural
        End Get
    End Property


    Public ReadOnly Property ShowDate As Boolean
        Get
            Return IsSermon
        End Get
    End Property
    Public ReadOnly Property DateString As String
        Get
            DateString = CUtilities.LongDate(ProductCreated)
            If ProductCreated.Hour = 1 Then
                DateString &= " AM"
            ElseIf ProductCreated.Hour = 12 Then
                DateString &= " PM"
            End If
        End Get
    End Property
    Public ReadOnly Property Am As Boolean?
        Get
            If ProductCreated.Hour = 1 Then Return True
            If ProductCreated.Hour = 12 Then Return False
            Return Nothing
        End Get
    End Property


    'Foreign key stuff
    Public ReadOnly Property ScribeName As String
        Get
            If Integer.MinValue = ProductScribeId Then Return String.Empty
            If IsNothing(Scribe) Then Return String.Empty
            Return Scribe.Name
        End Get
    End Property
    Public ReadOnly Property ImageScribeName As String
        Get
            If Integer.MinValue = ProductImageScribeId Then Return String.Empty
            If IsNothing(ImageScribe) Then Return String.Empty
            Return ImageScribe.Name
        End Get
    End Property
    Public ReadOnly Property ImageScribeUrl As String
        Get
            If Integer.MinValue = ProductImageScribeId Then Return String.Empty
            If IsNothing(ImageScribe) Then Return String.Empty
            Return ImageScribe.ScribeDisplayWebsite
        End Get
    End Property

    Public ReadOnly Property ProductPrice As Decimal
        Get
            If IsNothing(Button) Then Return 0
            Return Button.ButtonPrice
        End Get
    End Property
    Public ReadOnly Property ProductPaypalCode As String
        Get
            If IsNothing(Button) Then
                Return CButton.Cache.GetById(EButton.Donate).ButtonPaypalCode
            End If
            Return Button.ButtonPaypalCode
        End Get
    End Property
    Public ReadOnly Property ButtonName As String
        Get
            If IsNothing(Button) Then Return String.Empty
            Return Button.ButtonName
        End Get
    End Property
    Public ReadOnly Property HasParent As Boolean
        Get
            Return Not IsNothing(BelongsTo)
        End Get
    End Property

    <Obsolete()> Public ReadOnly Property HasVideo As Boolean
        Get
            Return HasVideos
        End Get
    End Property
    Public ReadOnly Property HasVimeoChannel As Boolean
        Get
            Return Not String.IsNullOrEmpty(ProductVimeoChannelId)
        End Get
    End Property
    Public ReadOnly Property HasYouTubePlaylist As Boolean
        Get
            Return Not String.IsNullOrEmpty(ProductYouTubePlaylistId)
        End Get
    End Property
    Public ReadOnly Property HasVimeo As Boolean
        Get
            Return ProductVimeoId > 0
        End Get
    End Property
    Public ReadOnly Property HasNotes As Boolean
        Get
            Return Not String.IsNullOrEmpty(ProductNotesAsPlainText) OrElse Files.HasHtm OrElse Files.HasPdfOrWord
        End Get
    End Property
    Public ReadOnly Property HasYouTube As Boolean
        Get
            Return Not String.IsNullOrEmpty(ProductYouTubeId)
        End Get
    End Property
    Public ReadOnly Property HasVideos As Boolean
        Get
            Return HasVideosOnVimeo OrElse HasVideosOnYouTube
        End Get
    End Property
    Public ReadOnly Property HasVideosOnBoth As Boolean
        Get
            Return HasVideosOnYouTube AndAlso HasVideosOnVimeo
        End Get
    End Property
    Public ReadOnly Property HasVideosOnYouTube As Boolean
        Get
            Return Not String.IsNullOrEmpty(ProductYouTubeId) OrElse Not String.IsNullOrEmpty(ProductYouTubePlaylistId)
        End Get
    End Property
    Public ReadOnly Property HasVideosOnVimeo As Boolean
        Get
            Return ProductVimeoId > 0 OrElse Not String.IsNullOrEmpty(ProductVimeoChannelId)
        End Get
    End Property

    Public ReadOnly Property HasBook As Boolean
        Get
            Return ProductCSBookIdOrParent > 0
        End Get
    End Property
    Public ReadOnly Property HasBook2 As Boolean
        Get
            Return ProductCSBookId2OrParent > 0
        End Get
    End Property
    Public ReadOnly Property HasBook3 As Boolean
        Get
            Return ProductCSBookId3OrParent > 0
        End Get
    End Property

    Public ReadOnly Property HasAudio As Boolean
        Get
            Return Files.HasMp3
        End Get
    End Property
    Public ReadOnly Property VideoUrl As String
        Get
            If HasVideosOnVimeo Then Return VideoUrl_Vimeo Else Return VideoUrl_YouTube 'Prefers Vimeo - method is redundant?
        End Get
    End Property
    Public ReadOnly Property VideoUrl_Vimeo As String
        Get
            If ProductVimeoId > 0 Then Return "https://vimeo.com/" & ProductVimeoId
            Return VideoUrl_VimeoChannel
        End Get
    End Property
    Public ReadOnly Property VideoUrl_YouTube As String
        Get
            If Not String.IsNullOrEmpty(ProductYouTubeId) Then Return "http://youtu.be/" & ProductYouTubeId
            Return VideoUrl_YouTubePlaylist
        End Get
    End Property
    Public ReadOnly Property VideoUrl_VimeoChannel As String
        Get
            If Not String.IsNullOrEmpty(ProductVimeoChannelId) Then Return "https://vimeo.com/channels/" & ProductVimeoChannelId
            Return CConfig.External_Vimeo
        End Get
    End Property
    Public ReadOnly Property VideoUrl_YouTubePlaylist As String
        Get
            If Not String.IsNullOrEmpty(ProductYouTubePlaylistId) Then Return "http://www.youtube.com/playlist?list=" & ProductYouTubePlaylistId
            Return CConfig.External_YouTube
        End Get
    End Property

    Public ReadOnly Property ProductCSBookIdOrParent As Integer
        Get
            If ProductCSBookId > 0 Then Return ProductCSBookId
            If HasParent Then Return BelongsTo.ProductCSBookId
            Return Integer.MinValue
        End Get
    End Property
    Public ReadOnly Property ProductCSBookId2OrParent As Integer
        Get
            If ProductCSBook2Id > 0 Then Return ProductCSBook2Id
            If HasParent Then Return BelongsTo.ProductCSBook2Id
            Return Integer.MinValue
        End Get
    End Property
    Public ReadOnly Property ProductCSBookId3OrParent As Integer
        Get
            If ProductCSBook3Id > 0 Then Return ProductCSBook3Id
            If HasParent Then Return BelongsTo.ProductCSBook3Id
            Return Integer.MinValue
        End Get
    End Property

    Protected ReadOnly Property CSMatched As CProductList
        Get
            If ProductCSBookIdOrParent > 0 Then
                Return CProduct.Cache.GetByCSBookId(ProductCSBookIdOrParent)
            End If
            Return New CProductList(0)
        End Get
    End Property
    Public ReadOnly Property CSPaperback As CProduct
        Get
            With CSMatched.Paperbacks
                If .Count > 0 Then Return .Item(0)
            End With
            Return Nothing
        End Get
    End Property


    Public ReadOnly Property ProductVimeoChannelIdOrParent As String
        Get
            If Not String.IsNullOrEmpty(ProductVimeoChannelId) Then Return ProductVimeoChannelId
            If HasParent Then Return BelongsTo.ProductVimeoChannelId
            Return String.Empty
        End Get
    End Property
    Public ReadOnly Property ProductYouTubePlaylistIdOrParent As String
        Get
            If Not String.IsNullOrEmpty(ProductYouTubePlaylistId) Then Return ProductYouTubePlaylistId
            If HasParent Then Return BelongsTo.ProductYouTubePlaylistId
            Return String.Empty
        End Get
    End Property
    Public ReadOnly Property ProductAmazonIdOrParent As String
        Get
            If Not String.IsNullOrEmpty(ProductAmazonId) Then Return ProductAmazonId
            If HasParent Then Return BelongsTo.ProductAmazonId
            Return String.Empty
        End Get
    End Property
    Public ReadOnly Property ProductSmashwordsIdOrParent As Integer
        Get
            If ProductSmashwordsId > 0 Then Return ProductSmashwordsId
            If HasParent Then Return BelongsTo.ProductSmashwordsId
            Return Integer.MinValue
        End Get
    End Property


    Public ReadOnly Property IsSet As Boolean
        Get
            Return IsConferenceSet 'CProductType.Cache.GetByParentTypeId(0).Count > 0
        End Get
    End Property
    Public ReadOnly Property CanHaveFiles As Boolean
        Get
            Return Not IsSet
        End Get
    End Property
    Public ReadOnly Property TemplateForProductInfo As EEmailTemplate
        Get
            If IsConferenceSet Then
                Return EEmailTemplate.Product_Info_ConferenceSet
            Else
                Return EEmailTemplate.Product_Info_ChildOrStandalone
            End If
        End Get
    End Property


    Private ReadOnly Property IsConferenceSet As Boolean
        Get
            Return ProductTypeId_ = EProductType.ConferenceSet
        End Get
    End Property
    Private ReadOnly Property IsManual As Boolean
        Get
            Return ProductTypeId_ = EProductType.Manual
        End Get
    End Property
    Private ReadOnly Property IsDocument As Boolean
        Get
            Return ProductTypeId_ = EProductType.Document
        End Get
    End Property
    Private ReadOnly Property IsSermon As Boolean
        Get
            Return ProductTypeId_ = EProductType.Sermon
        End Get
    End Property


    Public ReadOnly Property ProductKeywordsChineseSimplifiedOrParent As String
        Get
            Dim s As String = ProductKeywordsChineseSimplified
            If String.IsNullOrEmpty(s) AndAlso HasParent Then
                s = BelongsTo.ProductKeywordsChineseSimplified
            End If
            Return s
        End Get
    End Property
    Public ReadOnly Property ProductKeywordsChineseTraditionaldOrParent As String
        Get
            Dim s As String = ProductKeywordsChineseTraditionalOrSimplified
            If String.IsNullOrEmpty(s) AndAlso HasParent Then
                s = BelongsTo.ProductKeywordsChineseTraditionalOrSimplified
            End If
            Return s
        End Get
    End Property
    Public ReadOnly Property ProductKeywordsOrParent As String
        Get
            Dim s As String = ProductKeywords
            If String.IsNullOrEmpty(s) AndAlso HasParent Then
                s = BelongsTo.ProductKeywords
            End If
            Return s
        End Get
    End Property


    Public ReadOnly Property ProductDescriptionChineseSimplifiedOrParent As String
        Get
            Dim s As String = ProductDescriptionChineseSimplified
            If String.IsNullOrEmpty(s) AndAlso HasParent Then
                s = BelongsTo.ProductDescriptionChineseSimplified
            End If
            Return s
        End Get
    End Property
    Public ReadOnly Property ProductDescriptionChineseTraditionalOrParent As String
        Get
            Dim s As String = ProductDescriptionChineseTraditionalOrSimplified
            If String.IsNullOrEmpty(s) AndAlso HasParent Then
                s = BelongsTo.ProductDescriptionChineseTraditionalOrSimplified
            End If
            Return s
        End Get
    End Property
    Public ReadOnly Property ProductDescriptionOrParent As String
        Get
            Dim s As String = ProductDescription
            If String.IsNullOrEmpty(s) AndAlso HasParent Then
                s = BelongsTo.ProductDescription
            End If
            Return s
        End Get
    End Property


    Public ReadOnly Property HasImage As Boolean
        Get
            Return Not String.IsNullOrEmpty(ProductImageOrParent)
        End Get
    End Property
    Public ReadOnly Property ProductImageOrParent As String
        Get
            If Not String.IsNullOrEmpty(ProductImage) Then Return ProductImage
            If Not IsNothing(BelongsTo) Then Return BelongsTo.ProductImage
            Return String.Empty
        End Get
    End Property
    Public ReadOnly Property ProductThumbOrParent As String
        Get
            If Not String.IsNullOrEmpty(ProductThumb) Then Return ProductThumb
            If Not IsNothing(BelongsTo) Then Return BelongsTo.ProductThumb
            Return String.Empty
        End Get
    End Property
    Public ReadOnly Property ProductTinyOrParent As String
        Get
            If Not String.IsNullOrEmpty(ProductTiny) Then Return ProductTiny
            If Not IsNothing(BelongsTo) Then Return BelongsTo.ProductTiny
            Return String.Empty
        End Get
    End Property


    Public Property ProductTypeId_ As EProductType
        Get
            Return ProductTypeId
        End Get
        Set(value As EProductType)
            ProductTypeId = value
        End Set
    End Property
    Public ReadOnly Property ProductNameAndPrice As String
        Get
            If ProductPaypalButtonId = Integer.MinValue OrElse IsDonateButton Then
                Return CUtilities.Truncate(ProductName)
            Else
                Return String.Concat(CUtilities.Truncate(ProductName), " (", CTextbox.SetMoney(ProductPrice), ")")
            End If
        End Get
    End Property
    Public ReadOnly Property ProductNameAndLanguage As String
        Get
            Return String.Concat(ProductName, " (", LanguageName, ")")
        End Get
    End Property
    Public ReadOnly Property ProductNameAndPriceLong As String
        Get
            If ProductPaypalButtonId = Integer.MinValue Then
                Return ProductName
            Else
                Return String.Concat(ProductName, " (", CTextbox.SetMoney(ProductPrice), ")")
            End If
        End Get
    End Property
    Public ReadOnly Property ProductNameAndContents As String
        Get
            Return String.Concat(CUtilities.Truncate(ProductName), " (", Contents, ")")
        End Get
    End Property
    Public ReadOnly Property Contents() As String
        Get
            If IsSet Then
                Dim sb As New StringBuilder
                With Children
                    For Each i As CProductType In ProductType.ChildTypes
                        Dim list As CProductList = .GetByTypeId(i.TypeId).Active
                        If list.Count > 0 Then
                            If sb.Length > 0 Then sb.Append(", ")
                            sb.Append(CUtilities.CountSummary(list, i.TypeName.ToLower))
                        End If
                    Next
                End With
                If sb.Length = 0 Then sb.Append("no content!")
                Return sb.ToString.Replace("1 manual, ", "manual+")
            Else
                If Files.Count = 1 Then
                    If HasVideosOnBoth Then Return "Video and " & Files(0).Extension.ExtensionName
                    If HasVideosOnVimeo Then Return "Vimeo and " & Files(0).Extension.ExtensionName
                    If HasVideosOnYouTube Then Return "YouTube and " & Files(0).Extension.ExtensionName
                    Return Files(0).FileNameAndSizeShort
                End If
                If Files.Count = 0 Then
                    If HasVideosOnBoth Then Return "Video"
                    If HasVideosOnVimeo Then Return "Vimeo"
                    If HasVideosOnYouTube Then Return "YouTube"
                End If

                Dim sb As New StringBuilder
                For Each i As CFile In Files
                    If sb.Length > 0 Then sb.Append(", ")
                    sb.Append(IO.Path.GetExtension(i.FileName).Substring(1))
                Next
                Return sb.ToString
                'Return CUtilities.CountSummary(Files, "file", IIf(HasVideo, "video", "no files!"))
            End If
        End Get
    End Property

    'Fk - optional
    Public ReadOnly Property BelongsToName As String
        Get
            If IsNothing(BelongsTo) Then Return String.Empty
            Return BelongsTo.ProductName
        End Get
    End Property
    Public ReadOnly Property BelongsToNameAlt As String
        Get
            If IsNothing(BelongsTo) Then Return String.Empty
            Return BelongsTo.ProductNameChineseSimplified
        End Get
    End Property
    Public ReadOnly Property LocationName As String
        Get
            If IsNothing(Location) Then Return String.Empty
            Return Location.LocationName
        End Get
    End Property
    Public ReadOnly Property SpeakerName As String
        Get
            If IsNothing(Speaker) Then Return String.Empty
            Return Speaker.SpeakerName
        End Get
    End Property
    Public ReadOnly Property Speaker2Name As String
        Get
            If IsNothing(Speaker2) Then Return String.Empty
            Return Speaker2.SpeakerName
        End Get
    End Property
    Public ReadOnly Property SpeakerNames As String
        Get
            If IsNothing(Speaker2) Then Return SpeakerName
            Return String.Concat(SpeakerName, ", ", Speaker2.SpeakerName)
        End Get
    End Property

    'Fk - required
    Public ReadOnly Property LanguageName As String
        Get
            Return Language.LanguageName
        End Get
    End Property
    Public ReadOnly Property LanguageNative As String
        Get
            Return Language.LanguageNative
        End Get
    End Property

    Public ReadOnly Property HasTranscript As Boolean
        Get
            Return ProductScribeId > 0
        End Get
    End Property
    Public ReadOnly Property HasTranscriptChineseSimp As Boolean
        Get
            Return Not String.IsNullOrEmpty(m_productTranscriptionChineseSimplified)
        End Get
    End Property
    Public ReadOnly Property HasTranscriptChineseTrad As Boolean
        Get
            Return Not String.IsNullOrEmpty(m_productTranscriptionChineseTraditional)
        End Get
    End Property
    Public ReadOnly Property IsEnglish As Boolean
        Get
            Return ProductLanguageId = ELanguage.English
        End Get
    End Property
#End Region

#Region "Save/Delete Overrides"
    Public Overrides Sub Save(txOrNull As System.Data.IDbTransaction)

        If Not m_insertPending Then
            Dim old As CProduct = OriginalState(txOrNull)
            If Not String.IsNullOrEmpty(old.ProductTranscription) AndAlso old.ProductTranscription <> Me.ProductTranscription Then
                With New CHistory
                    .HistoryProductId = Me.ProductId
                    .HistoryTranscript = old.ProductTranscription
                    .Save()
                End With
            End If
        End If
        MyBase.Save(txOrNull)
    End Sub
    Public Overrides Sub Delete(ByVal txOrNull As IDbTransaction)
        If Orders_(txOrNull).Count > 0 Then
            ProductIsActive = False
            Save(txOrNull)
            Throw New Exception("Cannot delete a product once it has Orders (has been marked inactive, will not be shown on website)")
        End If

        'Use a transaction if none supplied
        If txOrNull Is Nothing Then
            BulkDelete(Me)
            Exit Sub
        End If

        'Cascade-Delete (all child collections)
        For Each i As CProduct In Me.Children_(txOrNull)
            i.ProductBelongsToProductId = Integer.MinValue
            i.Save(txOrNull)
        Next
        'Me.Children_(txOrNull).DeleteAll(txOrNull)
        Me.Files_(txOrNull).DeleteAll(txOrNull)
        Me.Suggestions(txOrNull).DeleteAll(txOrNull)

        'Normal Delete
        MyBase.Delete(txOrNull)
    End Sub


    Protected Overrides Sub ToXml_Autogenerated(ByVal w As System.Xml.XmlWriter)
        Store(w, "ProductId", Me.ProductId)
        Store(w, "ProductTypeId", Me.ProductTypeId)
        Store(w, "ProductSpeakerId", Me.ProductSpeakerId)
        Store(w, "ProductSpeaker2Id", Me.ProductSpeaker2Id)
        Store(w, "ProductLanguageId", Me.ProductLanguageId)
        Store(w, "ProductLocationId", Me.ProductLocationId)
        Store(w, "ProductBelongsToProductId", Me.ProductBelongsToProductId)
        Store(w, "ProductBelongsToSortOrder", Me.ProductBelongsToSortOrder)
        Store(w, "ProductName", Me.ProductName)
        Store(w, "ProductNameChineseSimplified", Me.ProductNameChineseSimplified)
        Store(w, "ProductNameChineseTraditional", Me.ProductNameChineseTraditional)
        Store(w, "ProductDescription", Me.ProductDescription)
        Store(w, "ProductDescriptionChineseSimplified", Me.ProductDescriptionChineseSimplified)
        Store(w, "ProductDescriptionChineseTraditional", Me.ProductDescriptionChineseTraditional)
        Store(w, "ProductKeywords", Me.ProductKeywords)
        Store(w, "ProductKeywordsChineseSimplified", Me.ProductKeywordsChineseSimplified)
        Store(w, "ProductKeywordsChineseTraditional", Me.ProductKeywordsChineseTraditional)
        Store(w, "ProductPaypalButtonId", Me.ProductPaypalButtonId)
        Store(w, "ProductImage", Me.ProductImage)
        Store(w, "ProductThumb", Me.ProductThumb)
        Store(w, "ProductTiny", Me.ProductTiny)
        Store(w, "ProductIsActive", Me.ProductIsActive)
        Store(w, "ProductScribeId", Me.ProductScribeId)
        Store(w, "ProductImageScribeId", Me.ProductImageScribeId)
        Store(w, "ProductTranscription", Me.ProductTranscription)
        Store(w, "ProductVimeoId", Me.ProductVimeoId)
        Store(w, "ProductYouTubeId", Me.ProductYouTubeId)
        Store(w, "ProductSmashwordsId", Me.ProductSmashwordsId)
        Store(w, "ProductAmazonId", Me.ProductAmazonId)
        Store(w, "ProductCSBookId", Me.ProductCSBookId)
        Store(w, "ProductCSBook2Id", Me.ProductCSBook2Id)
        Store(w, "ProductCSBook3Id", Me.ProductCSBook3Id)
        Store(w, "ProductNotesAsPlainText", Me.ProductNotesAsPlainText)
        Store(w, "ProductYouTubePlaylistId", Me.ProductYouTubePlaylistId)
        Store(w, "ProductVimeoChannelId", Me.ProductVimeoChannelId)
        Store(w, "ProductCreated", Me.ProductCreated)

        If IsNothing(ProductTranscription) Then
            Store(w, "ProductTranscription", 0)
        Else
            Store(w, "ProductTranscription", Me.ProductTranscription.GetHashCode)
        End If
        If IsNothing(ProductNotesAsPlainText) Then
            Store(w, "ProductNotesAsPlainText", 0)
        Else
            Store(w, "ProductNotesAsPlainText", Me.ProductNotesAsPlainText.GetHashCode)
        End If

        If IsNothing(ProductTranscriptionChineseSimplified) Then
            Store(w, "ProductTranscriptionChineseSimplified", 0)
        Else
            Store(w, "ProductTranscriptionChineseSimplified", Me.ProductTranscriptionChineseSimplified.GetHashCode)
        End If
        If IsNothing(ProductTranscriptionChineseTraditional) Then
            Store(w, "ProductTranscriptionChineseTraditional", 0)
        Else
            Store(w, "ProductTranscriptionChineseTraditional", Me.ProductTranscriptionChineseTraditional.GetHashCode)
        End If




    End Sub
#End Region

#Region "Custom Database Queries"
    '(Not normally required for cached classes, use list class for searching etc)
    'For Stored Procs can use: MakeListTyped (matching schema), or DataSrc.ExecuteDataset (reports etc)
    'For Dynamic sql, can use: SelectSum, SelectDistinct, SelectCount, SelectWhere (inherited methods)
    Private m_relatedProducts As CProductList
    Public ReadOnly Property RelatedProducts() As CProductList
        Get
            If IsNothing(m_relatedProducts) Then
                SyncLock (Me)
                    If IsNothing(m_relatedProducts) Then
                        m_relatedProducts = RelatedProductsFor(ProductId)
                    End If
                End SyncLock
            End If
            Return m_relatedProducts
        End Get
    End Property
    Public Function RelatedProductsFor(productId As Integer, Optional limit As Integer = 5) As CProductList
        Dim pi As New CPagingInfo(limit)
        pi.TableName = JOIN_RELATED
        pi.SortByColumn = "Score"
        pi.Descending = True
        Return SelectWhere(pi, New CCriteria("ThisProductId", productId))
    End Function
#End Region

#Region "Searching (Optional)"
    'For cached classes, custom seach logic resides in static methods on the list class
    ' e.g. CProduct.Cache.Search("..")

    'See also the auto-generated methods based on indexes
    ' e.g. CProduct.Cache.GetBy...
#End Region

#Region "Caching Details"
    'Cache Key
    Friend Shared CACHE_KEY As String = GetType(CProduct).ToString 'TABLE_NAME

    'Cache data
    Private Shared Function LoadCache() As CProductList
        If Not IsNothing(System.Web.HttpContext.Current) AndAlso System.Web.HttpContext.Current.Request.Url.Host.ToLower.Contains("localhost") Then
            Dim filePath As String = System.Web.HttpContext.Current.Server.MapPath("~/App_Data/products.bin")
            If IO.File.Exists(filePath) Then Return CBinary.DeserialiseFromBytesAndUnzip(IO.File.ReadAllBytes(filePath))
            Dim list As CProductList = New CProduct().SelectAll
            IO.File.WriteAllBytes(filePath, CBinary.SerialiseToBytesAndZip(list))
            Return list
        End If
        Return New CProduct().SelectAll()
    End Function

    'Cache Timeout
    Private Shared Sub SetCache(ByVal value As CProductList)
        If Not IsNothing(value) Then value.Sort()
        CCache.Set(CACHE_KEY, value) 'Optional parameter can override timeout (otherwise uses config-settings, which default to 3hrs)
    End Sub

    'Helper Method
    Private Function CacheGetById(ByVal list As CProductList) As CProduct
        Return list.GetById(Me.ProductId)
    End Function
#End Region

#Region "Cloning"
    Public Function Clone(ByVal target As CDataSrc, ByVal txOrNull As IDbTransaction) As CProduct ', parentId As Integer) As CProduct
        'Shallow copy: Copies the immediate record, excluding autogenerated Pks
        Dim copy As New CProduct(Me, target)

        'Deep Copy - Child Entities: Cloned children must reference their cloned parent
        'copy.SampleParentId = parentId

        copy.Save(txOrNull)

        'Deep Copy - Parent Entities: Cloned parents also clone their child collections
        'Me.Children.Clone(target, txOrNull, copy.ProductId)

        Return copy
    End Function
#End Region

#Region "ToXml"
    Protected Overrides Sub ToXml_Custom(ByVal w As System.Xml.XmlWriter)
        'Store(w, "Example", Me.Example)
    End Sub
#End Region

#Region "Photos By Date with Caption"
    Private m_duncanPhotos As Dictionary(Of String, String()) = Nothing
    Public ReadOnly Property DuncanPhotos As Dictionary(Of String, String())
        Get
            If ProductTypeId_ <> EProductType.Sermon Then Return New Dictionary(Of String, String())(0)
            If IsNothing(m_duncanPhotos) Then
                SyncLock (Me)
                    If IsNothing(m_duncanPhotos) Then
                        m_duncanPhotos = New Dictionary(Of String, String())(0)
                        'Sometimes morning and evening share the photos...
                        Dim dir As String = String.Concat(CConfig.DropboxPath, "/photos/duncan/", ProductCreated.ToString("dd-MM-yy"), "/")
                        If Not IO.Directory.Exists(dir) Then
                            If Not Am.HasValue Then Return m_duncanPhotos
                            '...and sometimes they are specific to one or the other
                            dir = String.Concat(CConfig.DropboxPath, "/photos/duncan/", ProductCreated.ToString("dd-MM-yy"), IIf(Am.Value, "am", "pm"), "/")
                            If Not IO.Directory.Exists(dir) Then Return m_duncanPhotos
                        End If
                        Dim files As String() = IO.Directory.GetFiles(dir)
                        If files.Length > 0 Then m_duncanPhotos(String.Empty) = files

                        For Each subfolder As String In IO.Directory.GetDirectories(dir)
                            Dim name As String = subfolder.Substring(dir.Length)
                            files = IO.Directory.GetFiles(subfolder)
                            If name.StartsWith("/") OrElse name.StartsWith("\") Then name = name.Substring(1)
                            If files.Length > 0 Then m_duncanPhotos(name) = files
                        Next
                    End If
                End SyncLock
            End If
            Return m_duncanPhotos
        End Get
    End Property
#End Region

End Class
